﻿using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace Purdue.Helper
{
    /// <summary>
    /// Attached property provider which adds the read-only attached property
    /// <c>TextBlockHelper.IsTextTrimmed</c> to the framework's <see cref="TextBlock"/> control.
    /// <para>
    /// Remember to use NotifyOnTargetUpdated=True for bindings,
    /// if you need the TextBlockHelper to react to changes to the TextBlock.Text property!
    /// </para>
    /// </summary>
    public static class TextBlockHelper
    {
        #region Constructors

        static TextBlockHelper()
        {
            
            // Register for the SizeChanged event on all TextBlocks, even if the event was handled.
            EventManager.RegisterClassHandler(
                typeof(TextBlock),
                FrameworkElement.SizeChangedEvent,
                new SizeChangedEventHandler(OnTextBlockSizeChanged),
                true);

            EventManager.RegisterClassHandler(
                typeof(TextBlock),
                Binding.TargetUpdatedEvent,
                new EventHandler<DataTransferEventArgs>(OnTargetUpdated),
                true);
        }

        #endregion (Constructors)

        #region Attached Property [TextBlockHelper.IsTextTrimmed]

        public static bool GetIsTextTrimmed(DependencyObject obj)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");

            return (bool)obj.GetValue(IsTextTrimmedProperty);
        }
        public static void SetIsTextTrimmed(DependencyObject obj, bool value)
        {
            if (obj == null)
                throw new ArgumentNullException("obj");

            obj.SetValue(IsTextTrimmedProperty, value);
        }
        public static readonly DependencyProperty IsTextTrimmedProperty =
            DependencyProperty.RegisterAttached("IsTextTrimmed", typeof(bool), typeof(TextBlockHelper));

        #endregion (Attached Property [TextBlockHelper.IsTextTrimmed])

        #region Attached Property [TextBlockHelper.AutomaticToolTipEnabled]

        /// <summary>
        /// Identifier associated with the attached property <c>AutomaticToolTipEnabled</c>.
        /// </summary>
        public static readonly DependencyProperty AutomaticToolTipEnabledProperty = DependencyProperty.RegisterAttached(
            "AutomaticToolTipEnabled",
            typeof(bool),
            typeof(TextBlockHelper),
            new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits));    // defaults to true

        /// <summary>
        /// Gets the current effective value of the AutomaticToolTipEnabled attached property.
        /// </summary>
        /// <param name="target"><see cref="TextBlock"/> to evaluate</param>
        /// <returns>Effective value of the AutomaticToolTipEnabled attached property</returns>
        [AttachedPropertyBrowsableForType(typeof(DependencyObject))]
        public static Boolean GetAutomaticToolTipEnabled(DependencyObject element)
        {
            if (null == element)
                throw new ArgumentNullException("element");

            return (bool)element.GetValue(AutomaticToolTipEnabledProperty);
        }

        /// <summary>
        /// Sets the current effective value of the AutomaticToolTipEnabled attached property.
        /// </summary>
        /// <param name="target"><see cref="TextBlock"/> to evaluate</param>
        /// <param name="value"><c>true</c> to enable the automatic ToolTip; otherwise <c>false</c></param>
        public static void SetAutomaticToolTipEnabled(DependencyObject element, bool value)
        {
            if (null == element)
                throw new ArgumentNullException("element");

            element.SetValue(AutomaticToolTipEnabledProperty, value);
        }

        #endregion (Attached Property [TextBlockHelper.AutomaticToolTipEnabled])

        #region Methods

        /// <summary>
        /// Event handler for TextBlock's SizeChanged routed event. Triggers evaluation of the
        /// IsTextTrimmed attached property.
        /// </summary>
        /// <param name="sender">Object where the event handler is attached</param>
        /// <param name="e">Event data</param>
        private static void OnTextBlockSizeChanged(object sender, SizeChangedEventArgs e)
        {
            TextBlock textBlock = sender as TextBlock;
            if (null == textBlock)
                return;

            if (e.WidthChanged)
            {
                EvaluateWordWrapPossible(textBlock);
                EvaluateIsTextTrimmed(textBlock, false);
            }
        }

        /// <summary>
        /// Event handler for TextBlock's TargetUpdated routed event. Triggers evaluation of the
        /// IsTextTrimmed attached property.
        /// </summary>
        /// <param name="sender">Object where the event handler is attached</param>
        /// <param name="e">Event data</param>
        private static void OnTargetUpdated(object sender, DataTransferEventArgs e)
        {
            TextBlock textBlock = sender as TextBlock;
            if (null == textBlock)
                return;

            if (e.Property.Name.Equals("Text"))
            {
                EvaluateWordWrapPossible(textBlock);
                EvaluateIsTextTrimmed(textBlock, true);
            }
        }

        /// <summary>
        /// EvaluateIsTextTrimmed
        /// </summary>
        /// <param name="textBlock"></param>
        /// <param name="ignoreArrangeValid"></param>
        private static void EvaluateIsTextTrimmed(TextBlock textBlock, bool ignoreArrangeValid)
        {
            if ((TextTrimming.None == textBlock.TextTrimming) /*|| (textBlock.TextWrapping != TextWrapping.NoWrap)*/)
            {
                SetIsTextTrimmed(textBlock, false);
            }
            else
            {
                SetIsTextTrimmed(textBlock, CalculateIsTextTrimmed(textBlock, ignoreArrangeValid));
            }
        }

        /// <summary>
        /// Determines whether or not the text in <paramref name="textBlock"/> is currently being
        /// trimmed due to width constraints.
        /// </summary>
        /// <remarks>Does not work properly when TextWrapping is set to WrapWithOverflow.</remarks>
        /// <param name="textBlock"><see cref="TextBlock"/> to evaluate</param>
        /// <param name="ignoreArrangeValid"><see cref="TextBlock"/> ignore the IsArrangeValid flag</param>
        /// <returns><c>true</c> if the text is currently being trimmed; otherwise <c>false</c></returns>
        private static bool CalculateIsTextTrimmed(TextBlock textBlock, bool ignoreArrangeValid)
        {
            if (!ignoreArrangeValid && !textBlock.IsArrangeValid)
                return GetIsTextTrimmed(textBlock);

            bool wrap = (textBlock.TextWrapping == TextWrapping.Wrap);

            Typeface typeface = new Typeface(
                textBlock.FontFamily,
                textBlock.FontStyle,
                textBlock.FontWeight,
                textBlock.FontStretch);

            if (wrap)
            {
                var formattedText = new FormattedText(
                    textBlock.Text,
                    CultureInfo.CurrentCulture,
                    textBlock.FlowDirection,
                    typeface,
                    textBlock.FontSize,
                    textBlock.Foreground);

                formattedText.MaxTextWidth = textBlock.ActualWidth;

                var height = formattedText.Height;

                if (textBlock.UseLayoutRounding)
                    height = Math.Round(height);

                return (height - textBlock.ActualHeight) > 0.0001;
            }
            else
            {
                // FormattedText is used to measure the whole width of the text held up by TextBlock container
                FormattedText formattedText = new FormattedText(
                    textBlock.Text,
                    CultureInfo.CurrentCulture,
                    textBlock.FlowDirection,
                    typeface,
                    textBlock.FontSize,
                    textBlock.Foreground);

                var width = formattedText.Width;

                if (textBlock.UseLayoutRounding)
                    width = Math.Round(width);

                // Sometimes the FormattedText width is a little bit difference with TextBlock's actualWidth.
                // Ignore differences that are very small due to rounding errors.            
                return (width - textBlock.ActualWidth) > 0.0001;
            }
        }

        public static bool CheckTextTrimmed(TextBlock textBlock)
        {
            if (textBlock == null)
                return false;

            return CalculateIsTextTrimmed(textBlock, true);
        }

        #endregion (Methods)

        #region Attached Property WholeWordWrap



        public static readonly DependencyProperty WholeWordWrapProperty =
            DependencyProperty.RegisterAttached("WholeWordWrap", typeof(bool), typeof(TextBlockHelper), new UIPropertyMetadata(false));

        /// <summary>
        /// Gets the current effective value of the WholeWordWrapEnabled attached property.
        /// </summary>
        /// <param name="target"><see cref="TextBlock"/> to evaluate</param>
        /// <returns>Effective value of the WholeWordWrap attached property</returns>
        [AttachedPropertyBrowsableForType(typeof(DependencyObject))]
        public static Boolean GetWholeWordWrap(DependencyObject element)
        {
            if (null == element)
                throw new ArgumentNullException("element");

            return (bool)element.GetValue(WholeWordWrapProperty);
        }

        /// <summary>
        /// Sets the current effective value of the WholeWordWrap attached property.
        /// </summary>
        /// <param name="target"><see cref="TextBlock"/> to evaluate</param>
        /// <param name="value"><c>true</c> to allow only word wrap with whole words.</param>
        public static void SetWholeWordWrap(DependencyObject element, bool value)
        {
            if (null == element)
                throw new ArgumentNullException("element");

            element.SetValue(WholeWordWrapProperty, value);
        }


        #endregion Attached Property NoInnerWordWrap

        #region methods

        /// <summary>
        /// Check if Wordwrap detection can work in the current language.
        /// </summary>
        /// <returns>True if the language use space to separate words.</returns>
        private static bool IsCurrentLanguageValid()
        {
            string langCode = //App.Current.UICulture.TwoLetterISOLanguageName;
                                CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
            switch (langCode)
            {
                case "zh": // Chinese
                //case "ja": // Japanese (character wrap is preferred to ellipsis, see WI #14185)
                case "ko": // Korean
                    return false;
                default:
                    return true;
            }
        }

        private static string GetFirstWord(string text)
        {
            // TODO/FIXME: 14851 , check if this actually works:
            // for "ja" language we need to return the first character as first word to enable character wrap
            string langCode = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
            if (langCode == "ja")
            {
                if (text.Length > 0)
                {
                    return string.Empty + text[0];
                }
            }

            string[] strings = text.Split(new Char[] { ' ', '/', ',', '.', ':', '\t' }, 2, StringSplitOptions.RemoveEmptyEntries);
            if (strings.Length >= 1)
            {
                return strings[0];
            }

            return string.Empty;
        }

        private static void EvaluateWordWrapPossible(TextBlock textBlock)
        {
            if (textBlock.IsArrangeValid == false)
                return;

            if (GetWholeWordWrap(textBlock) == false)
                return;

            if (IsCurrentLanguageValid() == false)
                return;

            Typeface typeface = new Typeface(
                textBlock.FontFamily,
                textBlock.FontStyle,
                textBlock.FontWeight,
                textBlock.FontStretch);

            // FormattedText is used to measure the whole width of the text held up by TextBlock container
            FormattedText formattedText = new FormattedText(
                GetFirstWord(textBlock.Text),
                CultureInfo.CurrentCulture,
                textBlock.FlowDirection,
                typeface,
                textBlock.FontSize,
                textBlock.Foreground);

            if ((formattedText.Width > textBlock.ActualWidth)
                && (formattedText.MinWidth > textBlock.ActualWidth))
            {
                textBlock.TextWrapping = TextWrapping.NoWrap;
            }
            else
            {
                textBlock.TextWrapping = TextWrapping.Wrap;
            }
        }

        #endregion methods
    }
}
